1   package org.apache.lucene.util;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one or more
5    * contributor license agreements.  See the NOTICE file distributed with
6    * this work for additional information regarding copyright ownership.
7    * The ASF licenses this file to You under the Apache License, Version 2.0
8    * (the "License"); you may not use this file except in compliance with
9    * the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  
20  import java.util.ArrayList;
21  
22  /**
23   * Basic reusable geo-spatial utility methods
24   *
25   * @lucene.experimental
26   */
27  public final class GeoUtils {
28    public static final short BITS = 31;
29    private static final double LON_SCALE = (0x1L<<BITS)/360.0D;
30    private static final double LAT_SCALE = (0x1L<<BITS)/180.0D;
31    public static final double TOLERANCE = 1E-6;
32  
33    /** Minimum longitude value. */
34    public static final double MIN_LON_INCL = -180.0D;
35  
36    /** Maximum longitude value. */
37    public static final double MAX_LON_INCL = 180.0D;
38  
39    /** Minimum latitude value. */
40    public static final double MIN_LAT_INCL = -90.0D;
41  
42    /** Maximum latitude value. */
43    public static final double MAX_LAT_INCL = 90.0D;
44  
45    // No instance:
46    private GeoUtils() {
47    }
48  
49    public static final Long mortonHash(final double lon, final double lat) {
50      return BitUtil.interleave(scaleLon(lon), scaleLat(lat));
51    }
52  
53    public static final double mortonUnhashLon(final long hash) {
54      return unscaleLon(BitUtil.deinterleave(hash));
55    }
56  
57    public static final double mortonUnhashLat(final long hash) {
58      return unscaleLat(BitUtil.deinterleave(hash >>> 1));
59    }
60  
61    private static final long scaleLon(final double val) {
62      return (long) ((val-MIN_LON_INCL) * LON_SCALE);
63    }
64  
65    private static final long scaleLat(final double val) {
66      return (long) ((val-MIN_LAT_INCL) * LAT_SCALE);
67    }
68  
69    private static final double unscaleLon(final long val) {
70      return (val / LON_SCALE) + MIN_LON_INCL;
71    }
72  
73    private static final double unscaleLat(final long val) {
74      return (val / LAT_SCALE) + MIN_LAT_INCL;
75    }
76  
77    public static double compare(final double v1, final double v2) {
78      final double delta = v1-v2;
79      return Math.abs(delta) <= TOLERANCE ? 0 : delta;
80    }
81  
82    /**
83     * Puts longitude in range of -180 to +180.
84     */
85    public static double normalizeLon(double lon_deg) {
86      if (lon_deg >= -180 && lon_deg <= 180) {
87        return lon_deg; //common case, and avoids slight double precision shifting
88      }
89      double off = (lon_deg + 180) % 360;
90      if (off < 0) {
91        return 180 + off;
92      } else if (off == 0 && lon_deg > 0) {
93        return 180;
94      } else {
95        return -180 + off;
96      }
97    }
98  
99    /**
100    * Puts latitude in range of -90 to 90.
101    */
102   public static double normalizeLat(double lat_deg) {
103     if (lat_deg >= -90 && lat_deg <= 90) {
104       return lat_deg; //common case, and avoids slight double precision shifting
105     }
106     double off = Math.abs((lat_deg + 90) % 360);
107     return (off <= 180 ? off : 360-off) - 90;
108   }
109 
110   /**
111    * Determine if a bbox (defined by minLon, minLat, maxLon, maxLat) contains the provided point (defined by lon, lat)
112    * NOTE: this is a basic method that does not handle dateline or pole crossing. Unwrapping must be done before
113    * calling this method.
114    */
115   public static boolean bboxContains(final double lon, final double lat, final double minLon,
116                                      final double minLat, final double maxLon, final double maxLat) {
117     return (compare(lon, minLon) >= 0 && compare(lon, maxLon) <= 0
118           && compare(lat, minLat) >= 0 && compare(lat, maxLat) <= 0);
119   }
120 
121   /**
122    * simple even-odd point in polygon computation
123    *    1.  Determine if point is contained in the longitudinal range
124    *    2.  Determine whether point crosses the edge by computing the latitudinal delta
125    *        between the end-point of a parallel vector (originating at the point) and the
126    *        y-component of the edge sink
127    *
128    * NOTE: Requires polygon point (x,y) order either clockwise or counter-clockwise
129    */
130   public static boolean pointInPolygon(double[] x, double[] y, double lat, double lon) {
131     assert x.length == y.length;
132     boolean inPoly = false;
133     /**
134      * Note: This is using a euclidean coordinate system which could result in
135      * upwards of 110KM error at the equator.
136      * TODO convert coordinates to cylindrical projection (e.g. mercator)
137      */
138     for (int i = 1; i < x.length; i++) {
139       if (x[i] < lon && x[i-1] >= lon || x[i-1] < lon && x[i] >= lon) {
140         if (y[i] + (lon - x[i]) / (x[i-1] - x[i]) * (y[i-1] - y[i]) < lat) {
141           inPoly = !inPoly;
142         }
143       }
144     }
145     return inPoly;
146   }
147 
148   public static String geoTermToString(long term) {
149     StringBuilder s = new StringBuilder(64);
150     final int numberOfLeadingZeros = Long.numberOfLeadingZeros(term);
151     for (int i = 0; i < numberOfLeadingZeros; i++) {
152       s.append('0');
153     }
154     if (term != 0) {
155       s.append(Long.toBinaryString(term));
156     }
157     return s.toString();
158   }
159 
160 
161   public static boolean rectDisjoint(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY,
162                                      final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) {
163     return (aMaxX < bMinX || aMinX > bMaxX || aMaxY < bMinY || aMinY > bMaxY);
164   }
165 
166   /**
167    * Computes whether the first (a) rectangle is wholly within another (b) rectangle (shared boundaries allowed)
168    */
169   public static boolean rectWithin(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY,
170                                    final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) {
171     return !(aMinX < bMinX || aMinY < bMinY || aMaxX > bMaxX || aMaxY > bMaxY);
172   }
173 
174   public static boolean rectCrosses(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY,
175                                     final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) {
176     return !(rectDisjoint(aMinX, aMinY, aMaxX, aMaxY, bMinX, bMinY, bMaxX, bMaxY) ||
177         rectWithin(aMinX, aMinY, aMaxX, aMaxY, bMinX, bMinY, bMaxX, bMaxY));
178   }
179 
180   /**
181    * Computes whether rectangle a contains rectangle b (touching allowed)
182    */
183   public static boolean rectContains(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY,
184                                      final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) {
185     return !(bMinX < aMinX || bMinY < aMinY || bMaxX > aMaxX || bMaxY > aMaxY);
186   }
187 
188   /**
189    * Computes whether a rectangle intersects another rectangle (crosses, within, touching, etc)
190    */
191   public static boolean rectIntersects(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY,
192                                        final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) {
193     return !((aMaxX < bMinX || aMinX > bMaxX || aMaxY < bMinY || aMinY > bMaxY) );
194   }
195 
196   /**
197    * Computes whether a rectangle crosses a shape. (touching not allowed)
198    */
199   public static boolean rectCrossesPoly(final double rMinX, final double rMinY, final double rMaxX,
200                                         final double rMaxY, final double[] shapeX, final double[] shapeY,
201                                         final double sMinX, final double sMinY, final double sMaxX,
202                                         final double sMaxY) {
203     // short-circuit: if the bounding boxes are disjoint then the shape does not cross
204     if (rectDisjoint(rMinX, rMinY, rMaxX, rMaxY, sMinX, sMinY, sMaxX, sMaxY)) {
205       return false;
206     }
207 
208     final double[][] bbox = new double[][] { {rMinX, rMinY}, {rMaxX, rMinY}, {rMaxX, rMaxY}, {rMinX, rMaxY}, {rMinX, rMinY} };
209     final int polyLength = shapeX.length-1;
210     double d, s, t, a1, b1, c1, a2, b2, c2;
211     double x00, y00, x01, y01, x10, y10, x11, y11;
212 
213     // computes the intersection point between each bbox edge and the polygon edge
214     for (short b=0; b<4; ++b) {
215       a1 = bbox[b+1][1]-bbox[b][1];
216       b1 = bbox[b][0]-bbox[b+1][0];
217       c1 = a1*bbox[b+1][0] + b1*bbox[b+1][1];
218       for (int p=0; p<polyLength; ++p) {
219         a2 = shapeY[p+1]-shapeY[p];
220         b2 = shapeX[p]-shapeX[p+1];
221         // compute determinant
222         d = a1*b2 - a2*b1;
223         if (d != 0) {
224           // lines are not parallel, check intersecting points
225           c2 = a2*shapeX[p+1] + b2*shapeY[p+1];
226           s = (1/d)*(b2*c1 - b1*c2);
227           t = (1/d)*(a1*c2 - a2*c1);
228           x00 = StrictMath.min(bbox[b][0], bbox[b+1][0]) - TOLERANCE;
229           x01 = StrictMath.max(bbox[b][0], bbox[b+1][0]) + TOLERANCE;
230           y00 = StrictMath.min(bbox[b][1], bbox[b+1][1]) - TOLERANCE;
231           y01 = StrictMath.max(bbox[b][1], bbox[b+1][1]) + TOLERANCE;
232           x10 = StrictMath.min(shapeX[p], shapeX[p+1]) - TOLERANCE;
233           x11 = StrictMath.max(shapeX[p], shapeX[p+1]) + TOLERANCE;
234           y10 = StrictMath.min(shapeY[p], shapeY[p+1]) - TOLERANCE;
235           y11 = StrictMath.max(shapeY[p], shapeY[p+1]) + TOLERANCE;
236           // check whether the intersection point is touching one of the line segments
237           boolean touching = ((x00 == s && y00 == t) || (x01 == s && y01 == t))
238               || ((x10 == s && y10 == t) || (x11 == s && y11 == t));
239           // if line segments are not touching and the intersection point is within the range of either segment
240           if (!(touching || x00 > s || x01 < s || y00 > t || y01 < t || x10 > s || x11 < s || y10 > t || y11 < t)) {
241             return true;
242           }
243         }
244       } // for each poly edge
245     } // for each bbox edge
246     return false;
247   }
248 
249   public static boolean lineCrossesPoly(double x1, double y1, double x2, double y2, double[] shapeX, double[] shapeY, final double sMinX, final double sMinY, final double sMaxX,
250                                         final double sMaxY) {
251     final double a1 = y2 - y1;
252     final double b1 = x2 - x1;
253     final double c1 = a1*x2 + b1*y2;
254     final int polyLength = shapeX.length;
255     double a2, b2, c2, s, t, d;
256     double x00, x01, y00, y01, x10, x11, y10, y11;
257     for (int p=0; p<polyLength; ++p) {
258       a2 = shapeY[p+1]-shapeY[p];
259       b2 = shapeX[p]-shapeX[p+1];
260       // compute determinant
261       d = a1*b2 - a2*b1;
262       if (d != 0) {
263         // lines are not parallel, check intersecting points
264         c2 = a2*shapeX[p+1] + b2*shapeY[p+1];
265         s = (1/d)*(b2*c1 - b1*c2);
266         t = (1/d)*(a1*c2 - a2*c1);
267         x00 = StrictMath.min(x1, x2) - TOLERANCE;
268         x01 = StrictMath.max(x1, x2) + TOLERANCE;
269         y00 = StrictMath.min(y1, y2) - TOLERANCE;
270         y01 = StrictMath.max(y1, y2) + TOLERANCE;
271         x10 = StrictMath.min(shapeX[p], shapeX[p+1]) - TOLERANCE;
272         x11 = StrictMath.max(shapeX[p], shapeX[p+1]) + TOLERANCE;
273         y10 = StrictMath.min(shapeY[p], shapeY[p+1]) - TOLERANCE;
274         y11 = StrictMath.max(shapeY[p], shapeY[p+1]) + TOLERANCE;
275         // check whether the intersection point is touching one of the line segments
276         boolean touching = ((x00 == s && y00 == t) || (x01 == s && y01 == t))
277             || ((x10 == s && y10 == t) || (x11 == s && y11 == t));
278         // if line segments are not touching and the intersection point is within the range of either segment
279         if (!(touching || x00 > s || x01 < s || y00 > t || y01 < t || x10 > s || x11 < s || y10 > t || y11 < t)) {
280           return true;
281         }
282       }
283     } // for each poly edge
284     return false;
285   }
286 
287   /**
288    * Converts a given circle (defined as a point/radius) to an approximated line-segment polygon
289    *
290    * @param lon longitudinal center of circle (in degrees)
291    * @param lat latitudinal center of circle (in degrees)
292    * @param radiusMeters distance radius of circle (in meters)
293    * @return a list of lon/lat points representing the circle
294    */
295   @SuppressWarnings({"unchecked","rawtypes"})
296   public static ArrayList<double[]> circleToPoly(final double lon, final double lat, final double radiusMeters) {
297     double angle;
298     // a little under-sampling (to limit the number of polygonal points): using archimedes estimation of pi
299     final int sides = 25;
300     ArrayList<double[]> geometry = new ArrayList();
301     double[] lons = new double[sides];
302     double[] lats = new double[sides];
303 
304     double[] pt = new double[2];
305     final int sidesLen = sides-1;
306     for (int i=0; i<sidesLen; ++i) {
307       angle = (i*360/sides);
308       pt = GeoProjectionUtils.pointFromLonLatBearing(lon, lat, angle, radiusMeters, pt);
309       lons[i] = pt[0];
310       lats[i] = pt[1];
311     }
312     // close the poly
313     lons[sidesLen] = lons[0];
314     lats[sidesLen] = lats[0];
315     geometry.add(lons);
316     geometry.add(lats);
317 
318     return geometry;
319   }
320 
321   /**
322    * Computes whether a rectangle is within a given polygon (shared boundaries allowed)
323    */
324   public static boolean rectWithinPoly(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
325                                        final double[] shapeX, final double[] shapeY, final double sMinX,
326                                        final double sMinY, final double sMaxX, final double sMaxY) {
327     // check if rectangle crosses poly (to handle concave/pacman polys), then check that all 4 corners
328     // are contained
329     return !(rectCrossesPoly(rMinX, rMinY, rMaxX, rMaxY, shapeX, shapeY, sMinX, sMinY, sMaxX, sMaxY) ||
330         !pointInPolygon(shapeX, shapeY, rMinY, rMinX) || !pointInPolygon(shapeX, shapeY, rMinY, rMaxX) ||
331         !pointInPolygon(shapeX, shapeY, rMaxY, rMaxX) || !pointInPolygon(shapeX, shapeY, rMaxY, rMinX));
332   }
333 
334   private static boolean rectAnyCornersOutsideCircle(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
335                                                      final double centerLon, final double centerLat, final double radiusMeters) {
336     return SloppyMath.haversin(centerLat, centerLon, rMinY, rMinX)*1000.0 > radiusMeters
337       || SloppyMath.haversin(centerLat, centerLon, rMaxY, rMinX)*1000.0 > radiusMeters
338       || SloppyMath.haversin(centerLat, centerLon, rMaxY, rMaxX)*1000.0 > radiusMeters
339       || SloppyMath.haversin(centerLat, centerLon, rMinY, rMaxX)*1000.0 > radiusMeters;
340   }
341 
342   private static boolean rectAnyCornersInCircle(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
343                                                 final double centerLon, final double centerLat, final double radiusMeters) {
344     return SloppyMath.haversin(centerLat, centerLon, rMinY, rMinX)*1000.0 <= radiusMeters
345         || SloppyMath.haversin(centerLat, centerLon, rMaxY, rMinX)*1000.0 <= radiusMeters
346         || SloppyMath.haversin(centerLat, centerLon, rMaxY, rMaxX)*1000.0 <= radiusMeters
347         || SloppyMath.haversin(centerLat, centerLon, rMinY, rMaxX)*1000.0 <= radiusMeters;
348   }
349 
350   public static boolean rectWithinCircle(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
351                                          final double centerLon, final double centerLat, final double radiusMeters) {
352     return rectAnyCornersOutsideCircle(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, radiusMeters) == false;
353   }
354 
355   /**
356    * Determine if a bbox (defined by minLon, minLat, maxLon, maxLat) contains the provided point (defined by lon, lat)
357    * NOTE: this is basic method that does not handle dateline or pole crossing. Unwrapping must be done before
358    * calling this method.
359    */
360   public static boolean rectCrossesCircle(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
361                                           final double centerLon, final double centerLat, final double radiusMeters) {
362     return rectAnyCornersInCircle(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, radiusMeters)
363         || isClosestPointOnRectWithinRange(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, radiusMeters);
364   }
365 
366   private static boolean isClosestPointOnRectWithinRange(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
367                                                          final double centerLon, final double centerLat, final double radiusMeters) {
368     double[] closestPt = {0, 0};
369     GeoDistanceUtils.closestPointOnBBox(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, closestPt);
370     return SloppyMath.haversin(centerLat, centerLon, closestPt[1], closestPt[0])*1000.0 <= radiusMeters;
371   }
372 
373   /**
374    * Compute Bounding Box for a circle using WGS-84 parameters
375    */
376   public static GeoRect circleToBBox(final double centerLon, final double centerLat, final double radiusMeters) {
377     final double radLat = StrictMath.toRadians(centerLat);
378     final double radLon = StrictMath.toRadians(centerLon);
379     double radDistance = radiusMeters / GeoProjectionUtils.SEMIMAJOR_AXIS;
380     double minLat = radLat - radDistance;
381     double maxLat = radLat + radDistance;
382     double minLon;
383     double maxLon;
384 
385     if (minLat > GeoProjectionUtils.MIN_LAT_RADIANS && maxLat < GeoProjectionUtils.MAX_LAT_RADIANS) {
386       double deltaLon = StrictMath.asin(StrictMath.sin(radDistance) / StrictMath.cos(radLat));
387       minLon = radLon - deltaLon;
388       if (minLon < GeoProjectionUtils.MIN_LON_RADIANS) {
389         minLon += 2d * StrictMath.PI;
390       }
391       maxLon = radLon + deltaLon;
392       if (maxLon > GeoProjectionUtils.MAX_LON_RADIANS) {
393         maxLon -= 2d * StrictMath.PI;
394       }
395     } else {
396       // a pole is within the distance
397       minLat = StrictMath.max(minLat, GeoProjectionUtils.MIN_LAT_RADIANS);
398       maxLat = StrictMath.min(maxLat, GeoProjectionUtils.MAX_LAT_RADIANS);
399       minLon = GeoProjectionUtils.MIN_LON_RADIANS;
400       maxLon = GeoProjectionUtils.MAX_LON_RADIANS;
401     }
402 
403     return new GeoRect(StrictMath.toDegrees(minLon), StrictMath.toDegrees(maxLon),
404         StrictMath.toDegrees(minLat), StrictMath.toDegrees(maxLat));
405   }
406 
407   public static GeoRect polyToBBox(double[] polyLons, double[] polyLats) {
408     if (polyLons.length != polyLats.length) {
409       throw new IllegalArgumentException("polyLons and polyLats must be equal length");
410     }
411 
412     double minLon = Double.POSITIVE_INFINITY;
413     double maxLon = Double.NEGATIVE_INFINITY;
414     double minLat = Double.POSITIVE_INFINITY;
415     double maxLat = Double.NEGATIVE_INFINITY;
416 
417     for (int i=0;i<polyLats.length;i++) {
418       if (GeoUtils.isValidLon(polyLons[i]) == false) {
419         throw new IllegalArgumentException("invalid polyLons[" + i + "]=" + polyLons[i]);
420       }
421       if (GeoUtils.isValidLat(polyLats[i]) == false) {
422         throw new IllegalArgumentException("invalid polyLats[" + i + "]=" + polyLats[i]);
423       }
424       minLon = Math.min(polyLons[i], minLon);
425       maxLon = Math.max(polyLons[i], maxLon);
426       minLat = Math.min(polyLats[i], minLat);
427       maxLat = Math.max(polyLats[i], maxLat);
428     }
429 
430     return new GeoRect(GeoUtils.unscaleLon(GeoUtils.scaleLon(minLon)), GeoUtils.unscaleLon(GeoUtils.scaleLon(maxLon)),
431         GeoUtils.unscaleLat(GeoUtils.scaleLat(minLat)), GeoUtils.unscaleLat(GeoUtils.scaleLat(maxLat)));
432   }
433 
434   /*
435   /**
436    * Computes whether or a 3dimensional line segment intersects or crosses a sphere
437    *
438    * @param lon1 longitudinal location of the line segment start point (in degrees)
439    * @param lat1 latitudinal location of the line segment start point (in degrees)
440    * @param alt1 altitude of the line segment start point (in degrees)
441    * @param lon2 longitudinal location of the line segment end point (in degrees)
442    * @param lat2 latitudinal location of the line segment end point (in degrees)
443    * @param alt2 altitude of the line segment end point (in degrees)
444    * @param centerLon longitudinal location of center search point (in degrees)
445    * @param centerLat latitudinal location of center search point (in degrees)
446    * @param centerAlt altitude of the center point (in meters)
447    * @param radiusMeters search sphere radius (in meters)
448    * @return whether the provided line segment is a secant of the
449    * /
450   // NOTE: not used for 2d at the moment. used for 3d w/ altitude (we can keep or add back)
451   private static boolean lineCrossesSphere(double lon1, double lat1, double alt1, double lon2,
452                                            double lat2, double alt2, double centerLon, double centerLat,
453                                            double centerAlt, double radiusMeters) {
454     // convert to cartesian 3d (in meters)
455     double[] ecf1 = GeoProjectionUtils.llaToECF(lon1, lat1, alt1, null);
456     double[] ecf2 = GeoProjectionUtils.llaToECF(lon2, lat2, alt2, null);
457     double[] cntr = GeoProjectionUtils.llaToECF(centerLon, centerLat, centerAlt, null);
458 
459     final double dX = ecf2[0] - ecf1[0];
460     final double dY = ecf2[1] - ecf1[1];
461     final double dZ = ecf2[2] - ecf1[2];
462     final double fX = ecf1[0] - cntr[0];
463     final double fY = ecf1[1] - cntr[1];
464     final double fZ = ecf1[2] - cntr[2];
465 
466     final double a = dX*dX + dY*dY + dZ*dZ;
467     final double b = 2 * (fX*dX + fY*dY + fZ*dZ);
468     final double c = (fX*fX + fY*fY + fZ*fZ) - (radiusMeters*radiusMeters);
469 
470     double discrim = (b*b)-(4*a*c);
471     if (discrim < 0) {
472       return false;
473     }
474 
475     discrim = StrictMath.sqrt(discrim);
476     final double a2 = 2*a;
477     final double t1 = (-b - discrim)/a2;
478     final double t2 = (-b + discrim)/a2;
479 
480     if ( (t1 < 0 || t1 > 1) ) {
481       return !(t2 < 0 || t2 > 1);
482     }
483 
484     return true;
485   }
486   */
487 
488   public static boolean isValidLat(double lat) {
489     return Double.isNaN(lat) == false && lat >= MIN_LAT_INCL && lat <= MAX_LAT_INCL;
490   }
491 
492   public static boolean isValidLon(double lon) {
493     return Double.isNaN(lon) == false && lon >= MIN_LON_INCL && lon <= MAX_LON_INCL;
494   }
495 }